import React, { useState, useMemo, useRef, useEffect, useCallback } from 'react';
import {
Info, Upload, Download, Printer, Plus, Trash2,
ChevronDown, Check, CheckCircle2, RotateCcw, AlertTriangle, Lock
} from 'lucide-react';
// --- CUSTOM HOOK PARA PERSISTÊNCIA OFFLINE (LOCALSTORAGE) ---
function useStickyState(defaultValue, key) {
const [value, setValue] = useState(() => {
try {
const stickyValue = window.localStorage.getItem(key);
return stickyValue !== null ? JSON.parse(stickyValue) : defaultValue;
} catch (error) {
console.warn("Error reading localStorage", error);
return defaultValue;
}
});
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error("Error setting localStorage (Quota exceeded?)", error);
if (error.name === 'QuotaExceededError') {
alert("Storage limit exceeded. Try uploading a smaller logo or clearing data.");
}
}
}, [key, value]);
return [value, setValue];
}
// --- INITIAL DATA ---
const PREDEFINED_STAGES = [
'STAGE 1 - START-UP', 'STAGE 2 - INITIATION', 'STAGE 3 - PREPARATION',
'STAGE 4 - MIGRATION', 'STAGE 5 - OPERATIONS'
];
const STAGE_DISPLAY_NAMES = {
'STAGE 1 - START-UP': 'STAGE 1 - START-UP',
'STAGE 2 - INITIATION': 'STAGE 2 - INITIATION',
'STAGE 3 - PREPARATION': 'STAGE 3 - PREPARATION',
'STAGE 4 - MIGRATION': 'STAGE 4 - MIGRATION',
'STAGE 5 - OPERATIONS': 'STAGE 5 - OPERATIONS'
};
const BASE_TIMELINE_DATA = [
{ id: 'TM-027', type: 'T-Minus', phase: 'STAGE 2 - INITIATION', tNum: 10, t: 'T-10', unit: 'Weeks', task: 'Provider to submit clean order forms for GSIP sites. Customer to follow-up with local provider for number portings.', ownerRole: 'Provider / Customer Delivery PM' },
{ id: 'TM-029', type: 'T-Minus', phase: 'STAGE 2 - INITIATION', tNum: 9, t: 'T-09', unit: 'Weeks', task: 'Order headsets, replacement phones, PSTN lines for modems, lifts, and security systems.', ownerRole: 'Customer Impl PM' },
{ id: 'TM-030', type: 'T-Minus', phase: 'STAGE 2 - INITIATION', tNum: 11, t: 'T-11', unit: 'Weeks', task: 'Order essential hardware (Gateways, Media Packs, Handsets).', ownerRole: 'Provider Delivery PM' },
{ id: 'TM-101', type: 'T-Minus', phase: 'STAGE 3 - PREPARATION', tNum: 8, t: 'T-08', unit: 'Weeks', task: 'Ensure all O365 E3/E5 and Phone System licences are allocated to end-users.', ownerRole: 'Customer O365 Admin' },
{ id: 'TM-102', type: 'T-Minus', phase: 'STAGE 3 - PREPARATION', tNum: 7, t: 'T-07', unit: 'Weeks', task: 'Provision Public IP addresses for SBC interfaces and configure network Firewalls.', ownerRole: 'Customer Network/Security' },
{ id: 'TM-103', type: 'T-Minus', phase: 'STAGE 3 - PREPARATION', tNum: 6, t: 'T-06', unit: 'Weeks', task: 'Build base SBC VM/Appliance and install required Public SSL Certificates.', ownerRole: 'Provider Impl Eng' },
{ id: 'TM-104', type: 'T-Minus', phase: 'STAGE 3 - PREPARATION', tNum: 5, t: 'T-05', unit: 'Weeks', task: 'Execute PowerShell pairing for SBC Direct Routing to Tenant.', ownerRole: 'Provider / Customer Teams Admin' },
{ id: 'TM-105', type: 'T-Minus', phase: 'STAGE 3 - PREPARATION', tNum: 3, t: 'T-03', unit: 'Weeks', task: 'Perform end-to-end PSTN test calls using restricted pilot user group.', ownerRole: 'Provider / Customer All' },
{ id: 'TM-106', type: 'T-Minus', phase: 'STAGE 3 - PREPARATION', tNum: 2, t: 'T-02', unit: 'Weeks', task: 'Readiness Sign-off: Executive confirmation to proceed with the migration window.', ownerRole: 'Customer Impl PM' },
{ id: 'RB-001', type: 'Runbook', phase: 'STAGE 1 - START-UP', tNum: 13, t: 'T-13', unit: 'Weeks', task: 'Schedule and host the official Site Implementation Kick-off session.', ownerRole: 'Provider Impl PM' },
{ id: 'RB-002', type: 'Runbook', phase: 'STAGE 1 - START-UP', tNum: 12, t: 'T-12', unit: 'Weeks', task: 'MEETING: Conduct Kick-off and align project milestones.', ownerRole: 'All' },
{ id: 'RB-003', type: 'Runbook', phase: 'STAGE 1 - START-UP', tNum: 12, t: 'T-12', unit: 'Weeks', task: 'Collect technical parameters and finalize the Site Information Document (SID).', ownerRole: 'Provider / Customer Impl Eng' },
{ id: 'RB-028', type: 'Runbook', phase: 'STAGE 2 - INITIATION', tNum: 11, t: 'T-11', unit: 'Weeks', task: 'Hardware quotations validated and approved by finance.', ownerRole: 'Provider Delivery PM' },
{ id: 'RB-031', type: 'Runbook', phase: 'STAGE 3 - PREPARATION', tNum: 10, t: 'T-10', unit: 'Weeks', task: 'Allocate IP subnets for local voice devices (AudioCodes, Poly, etc).', ownerRole: 'Customer Impl PM' },
{ id: 'RB-032', type: 'Runbook', phase: 'STAGE 3 - PREPARATION', tNum: 9, t: 'T-09', unit: 'Weeks', task: 'Draft and approve End-User migration communication templates.', ownerRole: 'Customer Impl PM' },
{ id: 'RB-033', type: 'Runbook', phase: 'STAGE 3 - PREPARATION', tNum: 2, t: 'T-02', unit: 'Weeks', task: 'Submit Change Management (CRQ) request for the migration event.', ownerRole: 'Customer Impl PM' },
{ id: 'RB-034', type: 'Runbook', phase: 'STAGE 3 - PREPARATION', tNum: 1, t: 'T-01', unit: 'Weeks', task: 'Pre-migration Go/No-Go checkpoint call with all stakeholders.', ownerRole: 'Provider / Customer All' },
{ id: 'RB-014', type: 'Runbook', phase: 'STAGE 4 - MIGRATION', tNum: 0, t: '01:30', unit: 'Hours', task: 'Redirect SIP traffic to Teams and execute automated user voice assignments.', ownerRole: 'Provider Impl Eng' },
{ id: 'RB-015', type: 'Runbook', phase: 'STAGE 4 - MIGRATION', tNum: 0, t: '03:30', unit: 'Hours', task: 'Migration UAT: Verify voice services on-site and provide sign-off.', ownerRole: 'Customer Impl PM' },
{ id: 'RB-016', type: 'Runbook', phase: 'STAGE 4 - MIGRATION', tNum: 0, t: '04:00', unit: 'Hours', task: 'Notification: Site successfully migrated to Teams Phone System.', ownerRole: 'Provider Impl PM' },
{ id: 'RB-017', type: 'Runbook', phase: 'STAGE 4 - MIGRATION', tNum: 0, t: '04:00', unit: 'Hours', task: 'Executive Report: Successful digital transformation of voice services.', ownerRole: 'Customer Impl PM' },
{ id: 'RB-018', type: 'Runbook', phase: 'STAGE 4 - MIGRATION', tNum: -0.1, t: 'Standby', unit: 'FDOB', task: 'Remote standby support during First Day of Business (FDOB).', ownerRole: 'Provider / Customer All' },
{ id: 'RB-019', type: 'Runbook', phase: 'STAGE 4 - MIGRATION', tNum: -0.1, t: 'Issues', unit: 'FDOB', task: 'Collect and raise migration related issues to Provider project team. Changes will only be performed after validation of the request', ownerRole: 'Customer Impl PM' },
{ id: 'RB-020', type: 'Runbook', phase: 'STAGE 4 - MIGRATION', tNum: -0.1, t: 'Fix', unit: 'FDOB', task: 'Troubleshoot ISSUES raised', ownerRole: 'Provider Impl Eng' },
{ id: 'RB-021', type: 'Runbook', phase: 'STAGE 4 - MIGRATION', tNum: -0.1, t: 'Review', unit: 'FDOB', task: 'Post Implementation review - Sign-off Migration', ownerRole: 'Provider Impl PM' },
{ id: 'RB-022', type: 'Runbook', phase: 'STAGE 4 - MIGRATION', tNum: -0.1, t: 'Actions', unit: 'FDOB', task: 'EMAIL: Document outstanding actions for the site and agree responsibilities', ownerRole: 'Provider Impl PM' },
{ id: 'RB-001-Ops', type: 'Runbook', phase: 'STAGE 5 - OPERATIONS', tNum: -0.2, t: 'Handover', unit: 'Post', task: 'Transition site to the 24/7 Global Operational Support Team.', ownerRole: 'Provider' },
{ id: 'RB-003-Ops', type: 'Runbook', phase: 'STAGE 5 - OPERATIONS', tNum: -0.2, t: 'BAU', unit: 'Post', task: 'Reinforce Business As Usual (BAU) support processes.', ownerRole: 'Customer' }
];
const DEFAULT_TASKS = BASE_TIMELINE_DATA.map(t => ({ ...t, manualStatus: 'Not Started', checked: false, deliveredDate: null }));
export default function App() {
// --- AUTHENTICATION STATE ---
const [isAuthenticated, setIsAuthenticated] = useState(() => {
return sessionStorage.getItem('migration_auth') === 'true';
});
const [passwordInput, setPasswordInput] = useState('');
const [authError, setAuthError] = useState(false);
// A SENHA FICA AQUI (Mude para o que desejar)
const SECRET_PASSWORD = "admin";
const handleLogin = (e) => {
e.preventDefault();
if (passwordInput === SECRET_PASSWORD) {
sessionStorage.setItem('migration_auth', 'true');
setIsAuthenticated(true);
setAuthError(false);
} else {
setAuthError(true);
}
};
const handleLogout = () => {
sessionStorage.removeItem('migration_auth');
setIsAuthenticated(false);
};
// TELA DE BLOQUEIO
if (!isAuthenticated) {
return (
Acesso Restrito
Insira a credencial para acessar o painel de migração.
);
}
// --- PERSISTENT STATE ---
const [tasks, setTasks] = useStickyState(DEFAULT_TASKS, 'migration_tasks_v1');
const [projectStart, setProjectStart] = useStickyState(new Date().toISOString().split('T')[0], 'migration_start_v1');
const [customerName, setCustomerName] = useStickyState('Customer', 'migration_cust_name_v1');
const [providerName, setProviderName] = useStickyState('Provider', 'migration_prov_name_v1');
const [customerLogo, setCustomerLogo] = useStickyState(null, 'migration_cust_logo_v1');
const [providerLogo, setProviderLogo] = useStickyState(null, 'migration_prov_logo_v1');
const [stakeholders, setStakeholders] = useStickyState([
{ id: 1, name: 'John Doe', team: 'Provider', role: 'Project Manager', contact: 'john.doe@provider.com' }
], 'migration_stakeholders_v1');
// --- VOLATILE STATE ---
const [filter, setFilter] = useState('All');
const [collapsedStages, setCollapsedStages] = useState({});
const [isInfoOpen, setIsInfoOpen] = useState(false);
const [addTaskStage, setAddTaskStage] = useState(null);
const [newTask, setNewTask] = useState({
id: '', type: 'Runbook', tNum: '', tDisplay: '', unit: 'Weeks', desc: '', owner: ''
});
const fileInputRef = useRef(null);
// --- PRINT STYLES (Injected once) ---
useEffect(() => {
const style = document.createElement('style');
style.innerHTML = `
@media print {
.no-print { display: none !important; }
body { background: white !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
main { max-width: 100% !important; padding: 0 !important; }
.shadow-md, .shadow-sm, .shadow-2xl { box-shadow: none !important; border: 1px solid #e5e7eb !important; }
header { position: static !important; border-bottom: 2px solid #1b222b !important; padding-bottom: 1rem !important; margin-bottom: 2rem !important; }
button, select, input[type="file"], label[for="csv-upload"] { display: none !important; }
input[type="text"], input[type="date"] { border: none !important; padding: 0 !important; background: transparent !important; }
.bg-white { background: white !important; }
.bg-\\[\\#f0efeb\\] { background: #fafafa !important; }
.print-border { border: 1px solid #d1d5db !important; }
.divide-y > * + * { border-top-width: 1px !important; border-color: #e5e7eb !important; }
}
`;
document.head.appendChild(style);
return () => document.head.removeChild(style);
}, []);
// --- DERIVED STATE ---
const tasksWithDates = useMemo(() => {
if (!projectStart) return tasks;
const startDate = new Date(projectStart + 'T12:00:00');
return tasks.map(task => {
let expected = new Date(startDate);
const isWeeks = task.unit === 'Weeks' || task.unit === 'Semanas';
const isHours = task.unit === 'Hours' || task.unit === 'Horas';
if (isWeeks) expected.setDate(expected.getDate() + ((13 - task.tNum) * 7));
else if (isHours) expected.setDate(expected.getDate() + (13 * 7));
else if (task.unit === 'FDOB') expected.setDate(expected.getDate() + (13 * 7) + 1);
else if (task.unit === 'Post') expected.setDate(expected.getDate() + (13 * 7) + 3);
else expected = null; // Fallback
return { ...task, expectedDate: expected };
});
}, [tasks, projectStart]);
const filteredTasks = useMemo(() => {
return tasksWithDates.filter(t => filter === 'All' || t.type === filter);
}, [tasksWithDates, filter]);
const progressPercent = useMemo(() => {
if (!tasksWithDates.length) return 0;
const completed = tasksWithDates.filter(t => t.checked).length;
return Math.round((completed / tasksWithDates.length) * 100);
}, [tasksWithDates]);
// --- HANDLERS ---
const handleLogoUpload = useCallback((e, setter) => {
const file = e.target.files[0];
if (file) {
if (file.size > 2 * 1024 * 1024) { // 2MB limit for localStorage safety
alert("File is too large. Please upload an image under 2MB.");
return;
}
const reader = new FileReader();
reader.onload = (ev) => setter(ev.target.result);
reader.readAsDataURL(file);
}
}, []);
const toggleCheck = useCallback((id) => {
if (!projectStart) {
alert("Please set the Project Kick-off Date first.");
return;
}
setTasks(prev => prev.map(t => {
if (t.id === id) {
const isChecked = !t.checked;
return {
...t,
checked: isChecked,
deliveredDate: isChecked ? new Date().toISOString() : null, // Save as ISO string for JSON
manualStatus: isChecked ? 'Completed' : 'Not Started'
};
}
return t;
}));
}, [projectStart, setTasks]);
const updateManualStatus = useCallback((id, status) => {
setTasks(prev => prev.map(t => t.id === id ? { ...t, manualStatus: status } : t));
}, [setTasks]);
const removeTask = useCallback((id) => {
if (window.confirm("Are you sure you want to permanently remove this action?")) {
setTasks(prev => prev.filter(t => t.id !== id));
}
}, [setTasks]);
const resetProject = useCallback(() => {
if (window.confirm("WARNING: This will erase all your custom tasks, stakeholders, and progress, resetting the project to default. Continue?")) {
setTasks(DEFAULT_TASKS);
setProjectStart(new Date().toISOString().split('T')[0]);
setCustomerName('Customer');
setProviderName('Provider');
setCustomerLogo(null);
setProviderLogo(null);
setStakeholders([{ id: 1, name: 'John Doe', team: 'Provider', role: 'Project Manager', contact: 'john.doe@provider.com' }]);
setIsInfoOpen(false);
}
}, [setTasks, setProjectStart, setCustomerName, setProviderName, setCustomerLogo, setProviderLogo, setStakeholders]);
const toggleStage = (stage) => setCollapsedStages(prev => ({ ...prev, [stage]: !prev[stage] }));
const expandCollapseAll = (expand) => {
const newState = {};
PREDEFINED_STAGES.forEach(s => newState[s] = !expand);
setCollapsedStages(newState);
};
const handleAddTaskSubmit = (e) => {
e.preventDefault();
const newTaskObj = {
id: newTask.id || `TSK-${Date.now().toString().slice(-4)}`,
type: newTask.type,
phase: addTaskStage,
tNum: parseFloat(newTask.tNum) || 0,
t: newTask.tDisplay || `T-${newTask.tNum}`,
unit: newTask.unit,
task: newTask.desc,
ownerRole: newTask.owner || 'Unassigned',
manualStatus: 'Not Started',
checked: false,
deliveredDate: null
};
setTasks(prev => [...prev, newTaskObj]);
setAddTaskStage(null);
setNewTask({ id: '', type: 'Runbook', tNum: '', tDisplay: '', unit: 'Weeks', desc: '', owner: '' });
};
// --- CSV LOGIC ---
const exportToCSV = () => {
let csvContent = "data:text/csv;charset=utf-8,\uFEFF";
csvContent += "ID,Type,Stage,T-Minus,Unit,Expected Date,Task Description,Action Owner,Status,Delivered Date\n";
filteredTasks.forEach(item => {
let expected = item.expectedDate ? new Date(item.expectedDate).toLocaleDateString('en-GB') : '';
let delivered = item.deliveredDate ? new Date(item.deliveredDate).toLocaleDateString('en-GB') : '';
let finalRole = item.ownerRole.replace(/Customer/gi, customerName).replace(/Provider/gi, providerName);
let taskEscaped = `"${item.task.replace(/"/g, '""')}"`;
csvContent += `"${item.id}","${item.type}","${item.phase}","${item.t}","${item.unit}","${expected}",${taskEscaped},"${finalRole}","${item.manualStatus}","${delivered}"\n`;
});
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
link.setAttribute("download", `migration_plan_${new Date().toISOString().split('T')[0]}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const parseCSVText = (text) => {
const result = [];
let row = [], cell = '', inQuotes = false;
for (let i = 0; i < text.length; i++) {
const char = text[i], nextChar = text[i + 1];
if (char === '"' && inQuotes && nextChar === '"') { cell += '"'; i++; }
else if (char === '"') inQuotes = !inQuotes;
else if (char === ',' && !inQuotes) { row.push(cell.trim()); cell = ''; }
else if ((char === '\n' || char === '\r') && !inQuotes) {
if (char === '\r' && nextChar === '\n') i++;
row.push(cell.trim()); result.push(row); row = []; cell = '';
}
else cell += char;
}
if (cell || row.length > 0) { row.push(cell.trim()); result.push(row); }
return result;
};
const parseDateToISO = (dateStr) => {
if(!dateStr) return null;
const parts = dateStr.split('/');
if(parts.length === 3) {
// Assuming DD/MM/YYYY from our export
return new Date(parts[2], parts[1] - 1, parts[0]).toISOString();
}
const d = new Date(dateStr);
return isNaN(d.getTime()) ? null : d.toISOString();
};
const importFromCSV = (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
try {
const text = ev.target.result;
const rows = parseCSVText(text);
if (rows.length < 2) throw new Error("Empty or invalid CSV structure.");
const newTimelineData = [];
for (let i = 1; i < rows.length; i++) {
const row = rows[i];
if (row.length < 9 || !row[0]) continue; // Skip malformed rows
const [id, type, phase, tDisplay, unit, , taskDesc, ownerRole, status, deliveredRaw] = row;
let tNum = 0;
const uLower = unit.toLowerCase();
if (uLower === 'weeks' || uLower === 'semanas') {
const match = tDisplay.match(/-?\d+(\.\d+)?/);
tNum = match ? parseFloat(match[0]) : 0;
// Adjust sign if T- was used
if (tDisplay.includes('T-') && tNum > 0) tNum = Math.abs(tNum);
} else if (uLower === 'fdob') tNum = -0.1;
else if (uLower === 'post') tNum = -0.2;
const isCompleted = status === 'Completed' || status === 'Concluído';
newTimelineData.push({
id: id.replace(/['"]/g, ''),
type: type.replace(/['"]/g, ''),
phase: phase.replace(/['"]/g, ''),
tNum,
t: tDisplay.replace(/['"]/g, ''),
unit: unit.replace(/['"]/g, ''),
task: taskDesc, // Handled by parser
ownerRole: ownerRole.replace(/['"]/g, ''),
manualStatus: isCompleted ? 'Completed' : (status ? status.replace(/['"]/g, '') : 'Not Started'),
checked: isCompleted,
deliveredDate: deliveredRaw ? parseDateToISO(deliveredRaw.replace(/['"]/g, '')) : null
});
}
if (newTimelineData.length > 0) {
setTasks(newTimelineData);
alert(`Successfully imported ${newTimelineData.length} tasks.`);
}
} catch (err) {
alert("Error parsing CSV: " + err.message);
} finally {
e.target.value = ''; // Reset input
}
};
reader.readAsText(file);
};
// --- RENDER HELPERS ---
const getBadgeClass = (str) => {
const s = str.toLowerCase();
if (s.includes(providerName.toLowerCase()) || s.includes('provider'))
return 'bg-[#2563eb]/10 text-[#2563eb] border-[#2563eb]/30 border';
if (s.includes(customerName.toLowerCase()) || s.includes('customer'))
return 'bg-[#1b222b]/5 text-[#1b222b] border-[#1b222b]/20 border';
return 'bg-[#9fb4c4]/10 text-[#1b222b]/70 border-[#9fb4c4]/30 border';
};
const getStatusSelectClass = (status) => {
switch(status) {
case 'In Progress': return 'bg-[#2563eb]/10 text-[#2563eb] border-[#2563eb]/30 hover:bg-[#2563eb]/20';
case 'Need Attention': return 'bg-[#e11d48]/10 text-[#e11d48] border-[#e11d48]/30 hover:bg-[#e11d48]/20';
default: return 'bg-white text-[#1b222b]/60 border-[#9fb4c4]/40 hover:border-[#1b222b]/40';
}
};
return (
{/* HEADER */}
{/* CUSTOMER SECTION */}
{/* TITLE */}
Migration Plan
Unified T-Minus & Runbook
{/* PROVIDER SECTION */}
{/* CONTROL BAR */}
setProjectStart(e.target.value)}
className="bg-gray-50 border border-gray-200 text-[#1b222b] rounded-lg px-3 py-2 outline-none font-bold focus:border-[#2563eb] focus:ring-2 focus:ring-[#2563eb]/20 text-sm transition-all"
/>
Progress
{progressPercent}%
{/* Circular progress visual representation */}
50 ? '150% -50%, 150% 150%, -50% 150%' : progressPercent + '% -50%'})`, transform: 'rotate(-45deg)'}}>
{/* TOP ROW: STAKEHOLDERS & LEGENDS */}
Action Owners Legend
Customer
Action or Approval required.
Provider
Engineering / Delivery.
Project Stakeholders
| Name |
Team |
Role |
Contact |
Action |
{stakeholders.map(s => {
const teamLabel = s.team.replace(/Provider/gi, providerName).replace(/Customer/gi, customerName);
return (
| {s.name} |
{teamLabel}
|
{s.role} |
{s.contact} |
|
)
})}
{stakeholders.length === 0 && (
| No stakeholders added yet. |
)}
{/* MACRO TIMELINE (PROGRESS) */}
Migration Phase Progress
{/* Background Track */}
{/* Fill Track */}
{[1, 2, 3, 4, 5].map(stageNum => {
const thresholds = [0, 15, 58, 73, 91];
const t = thresholds[stageNum-1];
const isDone = progressPercent >= (stageNum === 5 ? 100 : thresholds[stageNum]);
const isActive = progressPercent > t && !isDone;
const stageTasks = filteredTasks.filter(task => task.phase === PREDEFINED_STAGES[stageNum-1]);
let datesText = "No Target Dates";
if (stageTasks.length > 0) {
const times = stageTasks.filter(t => t.expectedDate).map(t => new Date(t.expectedDate).getTime());
if(times.length > 0) {
const min = new Date(Math.min(...times)).toLocaleDateString('en-GB', {day:'2-digit', month:'short'});
const max = new Date(Math.max(...times)).toLocaleDateString('en-GB', {day:'2-digit', month:'short'});
datesText = min === max ? min : `${min} - ${max}`;
}
}
return (
{isDone ? : stageNum}
STAGE {stageNum}
{STAGE_DISPLAY_NAMES[PREDEFINED_STAGES[stageNum-1]].split(' - ')[1]}
{/* Tooltip */}
{datesText}
);
})}
{/* GANTT CHART (CSS-BASED VISUAL) */}
Visual Schedule (Gantt Macro)
Phase Flow
T-13 WksT-10 WksT-6 WksT-2 WksMigrationPost-Ops
{/* Vertical Dashed Lines */}
{/* Bars */}
{[
{ label: 'S1: Start-Up', w: '20%', ml: '0%', bg: 'bg-gray-200', text: 'text-gray-700', border: 'border-gray-300' },
{ label: 'S2: Initiation', w: '40%', ml: '15%', bg: 'bg-[#2563eb]', text: 'text-white', border: 'border-[#2563eb]' },
{ label: 'S3: Preparation', w: '15%', ml: '50%', bg: 'bg-indigo-100', text: 'text-indigo-800', border: 'border-indigo-200' },
{ label: 'S4: Migration', w: '5%', ml: '65%', bg: 'bg-[#e11d48]', text: 'text-white', border: 'border-[#be123c]' },
{ label: 'S5: Operations', w: '25%', ml: '70%', bg: 'bg-[#55c977]/20', text: 'text-[#10b981]', border: 'border-[#55c977]/40' }
].map((bar, idx) => (
))}
{/* TASK LIST CONTROLS */}
Filter:
{/* TASK LIST CONTAINER */}
{filteredTasks.length === 0 ? (
No actions match the current filters.
Adjust your filters or add new tasks.
) : (
PREDEFINED_STAGES.map((stage) => {
const stageTasks = filteredTasks.filter(t => t.phase === stage).sort((a,b) => b.tNum - a.tNum);
if (stageTasks.length === 0) return null;
const isCollapsed = collapsedStages[stage] || false;
const stageProgress = Math.round((stageTasks.filter(t => t.checked).length / stageTasks.length) * 100);
return (
{/* Stage Header */}
toggleStage(stage)}
className="bg-gray-50/80 px-4 md:px-6 py-4 flex flex-wrap justify-between items-center cursor-pointer hover:bg-gray-100 transition-colors border-b border-gray-200 select-none"
>
{STAGE_DISPLAY_NAMES[stage]}
{stageProgress}% Done
{stageTasks.length} Items
{/* Tasks List */}
{!isCollapsed && (
{stageTasks.map(item => {
const finalTaskText = item.task.replace(/Provider/g, providerName).replace(/Customer/gi, customerName);
const finalRole = item.ownerRole.replace(/Customer/gi, customerName).replace(/Provider/gi, providerName);
const dFormat = item.expectedDate ? new Date(item.expectedDate).toLocaleDateString('en-GB') : 'N/A';
return (
{/* Left Indicator */}
{item.id}
{item.t}
{item.unit}
{/* Middle Meta */}
{finalTaskText}
{/* Type Badge */}
{item.type === 'T-Minus'
?
T-Minus
:
Runbook
}
{/* Date Badge */}
Target: {dFormat}
{/* Delivered Badge */}
{item.deliveredDate && (
{new Date(item.deliveredDate).toLocaleDateString('en-GB')}
)}
{/* Status Select */}
{!item.checked ? (
) : (
Done
)}
{/* Owner and Actions */}
👤 {finalRole}
{/* Right Checkbox (Desktop: side border, Mobile: top border) */}
{item.checked ? 'Completed' : 'Mark Complete'}
);
})}
)}
);
})
)}
{/* --- MODALS --- */}
{/* INFO MODAL */}
{isInfoOpen && (
setIsInfoOpen(false)}>
e.stopPropagation()}>
Project Framework
This React-powered console combines strategic planning (T-Minus) with tactical execution (Runbook) into a single chronology.
Data is saved securely offline in your browser's local storage.
⏳
T-Minus Strategy
Preparatory activities counting down to the migration event.
🚀
Execution Runbook
Technical tasks performed during the cutover window and support period.
)}
{/* ADD TASK MODAL */}
{addTaskStage && (
setAddTaskStage(null)}>
e.stopPropagation()}>
Add Task {STAGE_DISPLAY_NAMES[addTaskStage]}
)}
);
}